/* Copyright 2012 David Hadka
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */
package plot;

import algo.GreedySearch;
import lombok.Setter;
import model.tsp.Problem;
import model.tsp.Solution;
import reader.TspReader;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * Panel for displaying TSPLIB problem instances and tours.
 */
public class TSPPanel extends JPanel {

    private static final long serialVersionUID = -9001874665477567840L;

    /**
     * The TSPLIB problem instance.
     */
    private final Problem problem;

    /**
     * The displayed tours and their display settings.
     */
    private final Map<Solution, TourDisplaySetting> tours;

    /**
     * The width of nodes in the graphical display.
     */
    private double nodeWidth;

    /**
     * The border around the graphical display.  This border should be at least
     * half the node width to ensure nodes are fully contained inside the panel.
     */
    private Insets insets;

    /**
     * {@code true} if this graphical display should automatically repaint when
     * the displayed tours are changed; {@code false} otherwise.
     * -- SETTER --
     *  Set to
     *  if this graphical display should automatically
     *  repaint when the displayed tours are changed;
     *  otherwise.
     *  When
     * , the display will only change when
     *  is invoked or the component automatically repaints the panel.  This is
     *  used to avoid unnecessary repaints when making many changes to this
     *  display.
     *
     * @param autoRepaint {@code true} if this graphical display should
     *                    automatically repaint when the displayed tours are changed;
     *                    {@code false} otherwise

     */
    @Setter
    private boolean autoRepaint;

    /**
     * Constructs a new panel for displaying a TSPLIB problem instance.
     *
     * @param problem the TSPLIB problem instance
     */
    public TSPPanel(Problem problem) {
        super();
        this.problem = problem;

        tours = new LinkedHashMap<>();
        nodeWidth = 4.0;
        insets = new Insets((int) nodeWidth, (int) nodeWidth, (int) nodeWidth, (int) nodeWidth);
        autoRepaint = true;

        setBackground(Color.WHITE);
        setForeground(Color.BLACK);
    }

    /**
     * Adds a tour to this graphical display.  The tour will be displayed using
     * the default color.
     *
     * @param tour the tour to display
     */
	public void displayTour(Solution tour) {
		synchronized (tours) {
			tours.put(tour, new TourDisplaySetting());
		}

		if (autoRepaint) {
			repaint();
		}
	}

    /**
     * Adds a tour to this graphical display with the specified paint settings.
     *
     * @param tour the tour to display
     * @param paint the paint settings
     */
	public void displayTour(Solution tour, Paint paint) {
		synchronized (tours) {
			tours.put(tour, new TourDisplaySetting(paint));
		}

		if (autoRepaint) {
			repaint();
		}
	}

    /**
     * Adds a tour to this graphical display with the specified paint and
     * stroke settings.
     *
     * @param tour   the tour to display
     * @param paint  the paint settings
     * @param stroke the line stroke settings
     */
    public void displayTour(Solution tour, Paint paint, Stroke stroke) {
        synchronized (tours) {
            tours.put(tour, new TourDisplaySetting(paint, stroke));
        }

        if (autoRepaint) {
            repaint();
        }
    }

    /**
     * Removes all tours shown in this display.
     */
    public void clearTours() {
        synchronized (tours) {
            tours.clear();
        }

        if (autoRepaint) {
            repaint();
        }
    }

    /**
     * Removes the specified tour from this display.
     *
     * @param tour the tour to remove
     */
    public void removeTour(Solution tour) {
        synchronized (tours) {
            tours.remove(tour);
        }

        if (autoRepaint) {
            repaint();
        }
    }

    /**
     * Sets the width of nodes in the graphical display.
     *
     * @param nodeWidth the width of nodes in the graphical display
     */
    public void setNodeWidth(double nodeWidth) {
        this.nodeWidth = nodeWidth;

        if (autoRepaint) {
            repaint();
        }
    }

    /**
     * Sets the border around the graphical display.  This border should be at
     * least half the node width to ensure nodes are fully contained inside the
     * panel.
     *
     * @param insets the border around the graphical display
     */
    public void setInsets(Insets insets) {
        this.insets = insets;

        if (autoRepaint) {
            repaint();
        }
    }

    @Override
    public void paint(Graphics g) {
        synchronized (tours) {
            super.paint(g);
        }
    }

    @Override
    protected synchronized void paintComponent(Graphics g) {
        super.paintComponent(g);

        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);


        // first determine bounds of the data
        double left = Double.POSITIVE_INFINITY;
        double right = Double.NEGATIVE_INFINITY;
        double bottom = Double.POSITIVE_INFINITY;
        double top = Double.NEGATIVE_INFINITY;

        List<Integer> X = problem.getX();
        List<Integer> Y = problem.getY();
        for (int i = 0; i < problem.getNodeNum(); i++) {
            left = Math.min(left, X.get(i));
            right = Math.max(right, X.get(i));
            bottom = Math.min(bottom, Y.get(i));
            top = Math.max(top, Y.get(i));
        }

        // calculate the bounds of the drawing
        int displayWidth = getWidth();
        int displayHeight = getHeight();
        double scaleX = (displayWidth - insets.right - insets.left) / (right - left);
        double scaleY = (displayHeight - insets.top - insets.bottom) / (top - bottom);
        double scale = Math.min(scaleX, scaleY);
        double offsetX = (displayWidth - insets.right - insets.left - scale * (right - left)) / 2.0;
        double offsetY = (displayHeight - insets.top - insets.bottom - scale * (top - bottom)) / 2.0;

        // draw the tours
        for (Entry<Solution, TourDisplaySetting> entry : tours.entrySet()) {
            List<Integer> tour = entry.getKey().getPermutation();
            TourDisplaySetting displaySettings = entry.getValue();

            g2.setPaint(displaySettings.getPaint());
            g2.setStroke(displaySettings.getStroke());

            for (int i = 0; i < tour.size() - 1; i++) {
                int node1 = tour.get(i);
                int node2 = tour.get(i + 1);
                double[] position1 = new double[]{X.get(node1), Y.get(node1)};
                double[] position2 = new double[]{X.get(node2), Y.get(node2)};
                Line2D line = new Line2D.Double(
                        displayWidth - (offsetX + scale * (position1[0] - left) + insets.left),
                        displayHeight - (offsetY + scale * (position1[1] - bottom) + insets.bottom),
                        displayWidth - (offsetX + scale * (position2[0] - left) + insets.left),
                        displayHeight - (offsetY + scale * (position2[1] - bottom) + insets.bottom));

                g2.draw(line);
            }
            // 路径闭合
            int node1 = tour.get(tour.size() - 1);
            int node2 = tour.get(0);
            double[] position1 = new double[]{X.get(node1), Y.get(node1)};
            double[] position2 = new double[]{X.get(node2), Y.get(node2)};
            Line2D line = new Line2D.Double(
                    displayWidth - (offsetX + scale * (position1[0] - left) + insets.left),
                    displayHeight - (offsetY + scale * (position1[1] - bottom) + insets.bottom),
                    displayWidth - (offsetX + scale * (position2[0] - left) + insets.left),
                    displayHeight - (offsetY + scale * (position2[1] - bottom) + insets.bottom));

            g2.draw(line);
        }

        // draw the nodes
        g2.setColor(getForeground());

        for (int i = 0; i < problem.getNodeNum(); i++) {
            double[] position = new double[]{X.get(i), Y.get(i)};

            Ellipse2D point = new Ellipse2D.Double(
                    displayWidth - (offsetX + scale * (position[0] - left) + insets.left) - (nodeWidth / 2.0),
                    displayHeight - (offsetY + scale * (position[1] - bottom) + insets.bottom) - (nodeWidth / 2.0),
                    nodeWidth,
                    nodeWidth);

            g2.fill(point);
            g2.draw(point);
        }
    }

    public static void main(String[] args) throws IOException {
        String filename = "tsp/eil51.tsp";
        Problem problem = new TspReader().read(filename);
        GreedySearch algo = new GreedySearch(problem);
        Solution solution = algo.search();

        TSPPanel panel = new TSPPanel(problem);
        panel.displayTour(solution, Color.BLACK, new BasicStroke(2.0f));

        JFrame frame = new JFrame();
        frame.getContentPane().setLayout(new BorderLayout());
        frame.getContentPane().add(panel, BorderLayout.CENTER);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setSize(500, 400);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }


    /**
     * The inner class storing tour display settings.  These settings control
     * the paint and line stroke when rendering the tour.
     */
    private class TourDisplaySetting {

        /**
         * The paint/color used when rendering the tour.
         */
        private final Paint paint;

        /**
         * The line stroke used when rendering the tour.
         */
        private final Stroke stroke;

        /**
         * Constructs a new, default tour display setting.
         */
        public TourDisplaySetting() {
            this(Color.RED);
        }

        /**
         * Constructs a new tour display setting with the specified paint.
         *
         * @param paint the paint/color used when rendering the tour
         */
        public TourDisplaySetting(Paint paint) {
            this(paint, new BasicStroke());
        }

        /**
         * Constructs a new tour display setting with the specified paint and
         * line stroke.
         *
         * @param paint  the paint/color used when rendering the tour
         * @param stroke the line stroke used when rendering the tour
         */
        public TourDisplaySetting(Paint paint, Stroke stroke) {
            super();
            this.paint = paint;
            this.stroke = stroke;
        }

        /**
         * Returns the paint/color used when rendering the tour.
         *
         * @return the paint/color used when rendering the tour
         */
        public Paint getPaint() {
            return paint;
        }

        /**
         * Returns the line stroke used when rendering the tour.
         *
         * @return the line stroke used when rendering the tour
         */
        public Stroke getStroke() {
            return stroke;
        }

    }

}